Skip to content

fix(lwdid): preserve fitted overall inference - #1

Closed
shawcharles wants to merge 6 commits into
gorgeousfish:feature/lwdid-estimatorfrom
shawcharles:fix/lwdid-preserve-canonical-aggregation
Closed

fix(lwdid): preserve fitted overall inference#1
shawcharles wants to merge 6 commits into
gorgeousfish:feature/lwdid-estimatorfrom
shawcharles:fix/lwdid-preserve-canonical-aggregation

Conversation

@shawcharles

@shawcharles shawcharles commented Jul 28, 2026

Copy link
Copy Markdown

Summary

Implements the bounded result-provenance fix in igerber#733.

LWDiD.fit() already computes and stores the canonical overall ATT and its inference. LWDiDResults.aggregate(\"overall\") previously replaced that result with a separate aggregation built from marginal cohort effects and their standard errors. That could change the point estimate and discarded the composite-regression joint inference where it was available.

This change returns an equivalent result object containing the fitted canonical result instead of reconstructing a second estimator.

This does not establish the validity of the non-composite delta-method variance used by fit() for other staggered paths. That path still omits cross-cohort covariance; this PR ensures aggregate() no longer hides its provenance by replacing it with a different calculation. The remaining statistical work is tracked in igerber#735.

Test

  • Adds an end-to-end staggered regression test for the RA/classical/never-treated composite path.
  • The test asserts exact agreement of ATT, SE, t-statistic, p-value, confidence interval, and df_inference between fit() and aggregate(\"overall\").
  • Adds a slow, strict-xfail unit-cluster-bootstrap concordance test for non-composite staggered inference. It documents the covariance-aware target for LWDiD: use covariance-aware inference for staggered overall ATT igerber/diff-diff#735 and must be removed when that implementation passes it.
  • tests/test_lwdid.py: 64 passed.
  • The covariance test: expected xfail.
  • Black check passed for touched files.

This PR targets the source branch of igerber#588 so it can be merged there directly.

gorgeousfish and others added 5 commits July 17, 2026 10:38
Maintainer rebase onto current main (igerber, 2026-07-17), per plan agreed
in PR igerber#588: dropped committed datasets (tutorial now uses the checksummed
load_prop99()/load_walmart() loaders on main), kept the maintainer-authored
paper reviews and references entries from igerber#685, removed the lwdid dev
dependency (external reference implementations stay environmental,
importorskip-gated), renumbered tutorial 26 -> 27.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Fix IPW SE centering (translation-invariant influence function)
- Implement composite regression (Eq 7.18/7.19) for staggered ATT
- Add N_infinity >= 2 guard for never-treated controls
- Implement event study (Appendix D): WATT(r) + Algorithm 1 sup-t bands
- Fix randomization inference p-value convention (strict > comparison)
- Add design validation (absorbing treatment, time-invariant cohort)
- Migrate custom exceptions to ValueError
- Trim top-level exports to LWDiD/LWDiDResults/LW

Acceptance: 39 pass / 9 xfail (IPWRA event-study variants pending).
Remaining xfails are IPWRA-specific event-study cases for Step 3.
- Execute all 30 code cells with full outputs (text + plots)
- Add missing top-level exports to __init__.py:
  randomization_inference, wild_cluster_bootstrap,
  test_parallel_trends, sensitivity_analysis,
  recommend_transformation
…puts

- Fix IPWRA event-study golden tests (pass controls in test helper)
- Resolve mypy type errors to zero (type: ignore for pandas Union types)
- Execute tutorial notebook with outputs (30/30 cells)
- Fix tutorial imports to use module-level paths (per export trim)
- Remove stale XFAIL_IPW_CENTERING marker

Methodology tests: 43 pass / 5 xfail (non-strict bootstrap SE only).
All strict acceptance criteria met.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Re-trigger cubic

@gorgeousfish
gorgeousfish force-pushed the feature/lwdid-estimator branch from c2694ff to 10c900a Compare July 30, 2026 05:27
gorgeousfish added a commit that referenced this pull request Jul 30, 2026
…te contract

Four groups, one per issue in this round.

TestCohortTimeCellSupport pins the (g,t) construction (igerber#734): eligibility
is period-specific rather than per-cohort and matches never-treated plus
{G > max(g,t)} for every cell; a pure common time trend gives ATT = 0; a
constant shift applied to every unit-period leaves the ATT unchanged;
unsupported cells are reported with a reason and a cohort with no
supported cell is dropped.

TestInfluenceFunctionReconciliation checks that the influence function
each estimator returns actually reproduces that estimator's reported SE,
with and without covariates, and that psm reports no influence function
instead of a silently independent one.

TestStaggeredJointInference covers the overall SE (igerber#735): the joint basis
is reported, the SE is wider than the cohort-independence formula it
replaces, and it agrees with a unit-cluster bootstrap.

TestAggregationContract covers igerber#733 and igerber#732: aggregate("simple")
reproduces fit() exactly including df_inference and across every
inference basis, "group" carries cohort effects on the shared schema,
"event_study" returns EventStudyResults with the anchor as a reference row
and survives to_dict(), and the rejected cases (unknown type, weights
selector, balance_e off event_study, common-timing fits) all fail closed.

test_simple_preserves_the_fitted_result and
test_matches_unit_cluster_bootstrap are adapted from Charles Shaw's patch
in #1, which independently identified the
aggregate() SE discrepancy behind igerber#733.

Co-authored-by: Charles Shaw <shawcharles@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@gorgeousfish

Copy link
Copy Markdown
Owner

Thanks for this, @shawcharles — and for filing igerber#732, igerber#733, igerber#734 and igerber#735. The aggregate() SE discrepancy you caught in igerber#733 was real: the old aggregate(by="overall") rebuilt the ATT from the marginal cohort effects and took its SE as sqrt(sum w_g^2 se_g^2), so for the same estimand it reported a narrower SE than fit() and dropped df_inference along the way.

I should explain why I am not merging this patch directly. Addressing igerber#734 required replacing the staggered estimation path wholesale rather than patching it. The old code applied control eligibility as a unit-level filter and then averaged unequal calendar windows across cohorts, which is what let a pure common time trend come through as ATT ≈ 1.0 with SE ≈ 5e-16. Estimation now runs one cell per (g, t) in a new module diff_diff/lwdid_staggered.py, with control eligibility evaluated at t. That removed _aggregate_legacy — the function your patch modifies — so the diff no longer applies to the current branch.

The behaviour you were asserting is in place, and both of your tests came across with it:

  • Your exact fit() vs aggregate() agreement test is now TestAggregationContract::test_simple_preserves_the_fitted_result. It asserts exact equality on att, se, t_stat, p_value, both confidence bounds, df_inference and alpha. The entry point is aggregate("simple") rather than aggregate("overall"), because LWDiDResults now inherits the library's shared AggregationMixin and takes its vocabulary; "overall" raises and names the supported set. I also added a parametrized version over vce and control_group so the property is checked off the composite-regression path, not only where it is gated on.
  • Your unit-cluster-bootstrap concordance test is now TestStaggeredJointInference::test_matches_unit_cluster_bootstrap, checking the overall SE against a bootstrap over unit clusters. This is what pins the LWDiD: use covariance-aware inference for staggered overall ATT igerber/diff-diff#735 fix: cohort effects that share controls are correlated, so the overall SE is now read off a joint influence function accumulated across (g, t) cells rather than assuming cohort independence.

Both are in commit ac9667d5 on feature/lwdid-estimator, which carries you as Co-authored-by, and the commit message credits this PR by number for independently identifying the igerber#733 discrepancy. There is a summary of the whole round at igerber#588.

So this PR can be closed as superseded — I'll leave that to you rather than closing it myself. Thanks again for the review; the four issues materially improved the estimator.

@shawcharles

Copy link
Copy Markdown
Author

Thanks for the detailed explanation, and for carrying the two behavioural tests forward into ac9667d5 with co-author credit.

Since the _aggregate_legacy path this PR patched has now been replaced by the (g, t) staggered implementation, and the fit()/aggregate() contract plus joint-inference behaviour are covered directly on feature/lwdid-estimator, I am closing this as superseded.

I will keep review attention on igerber#588 and the remaining CI/review surface there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants